package com.idol.logger.core

import android.util.Log
import com.idol.logger.IdolLogger
import com.idol.logger.core.format.IdolStackTraceFormat
import com.idol.logger.core.format.IdolThreadFormat
import com.idol.logger.core.format.iface.IStackTraceFormat
import com.idol.logger.core.format.iface.IThreadFormat
import com.idol.logger.core.format.utils.croppedRealStackTrack
import com.idol.logger.core.json.IdolToJsonString
import com.idol.logger.core.json.iface.IdolToJson
import com.idol.logger.core.printer.IdolConsolePrinter
import com.idol.logger.core.printer.iface.IdolPrinter

internal class IdolManager private constructor() {

    //region ##### companion object
    companion object {
        internal const val GLOBAL_TAG = "idol"
        internal const val SELF_DEBUG = false
        var debug: Boolean = false
        var tag: String = GLOBAL_TAG
        var includeThread = false
        var stackTraceDepth = 0
        val instance by lazy { IdolManager() }
    }
    //endregion

    //region ##### members

    /**
     * printers
     */
    private val idolPrinters: MutableMap<String, IdolPrinter> = mutableMapOf()

    /**
     * stack trace formatter
     */
    var stackTraceFormat: IStackTraceFormat? = null
        get() = if (field != null) field else IdolStackTraceFormat()

    /**
     * thread formatter
     */
    var threadFormat: IThreadFormat? = null
        get() = if (field != null) field else IdolThreadFormat()

    /**
     * contents formatter
     */
    var messageFormat: IdolToJson? = null
        get() = field ?: IdolToJsonString()

    /**
     * ignorePackage
     */
    private val packageName by lazy {
        IdolLogger::class.java.name.let {
            val index = it.lastIndexOf('.')
            if (index > 0) it.substring(0, it.lastIndexOf('.')) else it
        }
    }
    //endregion

    //region ##### public function

    /**
     * add printers
     */
    fun putAll(vararg printers: IdolPrinter) = printers.forEach { put(it) }

    /**
     * add printers
     */
    fun putAll(printers: List<IdolPrinter>) = printers.forEach { put(it) }

    /**
     * add printer
     * @return add succeed - true, failed - false
     */
    fun put(printer: IdolPrinter) = with(printer.javaClass.simpleName) {
        if (!idolPrinters.containsKey(this)) idolPrinters[this] = printer
    }

    /**
     * print log
     * @param level         level {@code Log.VERBOSE、Log.DEBUG、Log.INFO、Log.ERROR、Log.WARN}
     * @param tag           tag
     * @param contents      messages
     */
    fun log(level: Int, tag: String, vararg contents: Any) {
        if (!debug) return
        StringBuilder().apply {
            threadInfo { append(it).append("\n") }
            stackTraceInfo { append(it).append("\n") }
            parseContent(*contents) { append(it) }
            print(level, tag, content = toString())
        }
    }
    //endregion

    //region ##### private function

    /**
     * get printers
     * @return all printers; when idolPrinters is empty, return default printer(IdolConsolePrinter)
     */
    private fun ofPrinters() = idolPrinters.values.let { collection ->
        if (collection.isEmpty()) {
            mutableListOf(IdolConsolePrinter)
        } else {
            collection
        }
    }

    /**
     * thread info
     * @param block (thread info):Unit => {}
     * @return thread info
     */
    private fun threadInfo(block: (String) -> Unit) = also {
        if (includeThread && debug) block.invoke(threadFormat!!.format(Thread.currentThread()))
    }

    /**
     * stack trace info
     * @param block (stack trace info):Unit => {}
     * @return stack trace info
     */
    private fun stackTraceInfo(block: (String) -> Unit) = also {
        if (stackTraceDepth > 0 && debug) {
            // ignore myself package
            if (SELF_DEBUG) Log.i(GLOBAL_TAG, "ignorePackage: $packageName")

            val stackTrace = croppedRealStackTrack(
                Throwable().stackTrace,
                packageName,
                stackTraceDepth
            )
            block.invoke(stackTraceFormat!!.format(stackTrace))
        }
    }

    /**
     * Any -> String
     */
    private fun parseContent(vararg contents: Any, block: (String) -> Unit) = also {
        if (debug) block.invoke(messageFormat!!.toJson(*contents))
    }

    /**
     * print log.
     */
    private fun print(level: Int, tag: String, content: String) {
        if (debug) ofPrinters().forEach { it.print(level, tag, content) }
    }
    //endregion

}